PR-AgentとBedrockでGitHubのPRをAIにレビューしてもらう

PR-AgentとBedrockでGitHubのPRをAIにレビューしてもらう

Clock Icon2024.11.06

はじめに

PR-AgentはPRレビューのAI補助ツールです。

本記事で登場するコードは以下のリポジトリに記載しています。全体像が掴みにくくなった際にご活用ください。

https://github.com/shuntaka9576/pr-agent-sample

Qodo Merge(旧PR-Agent)とは

2024年11月現在Qodo Mergeという名前に変わっているため、旧PR-Agentとなります。リネーム直後ということもあり、本記事では旧名で記載します。

PRレビュー系の生成AIツールは、他にCodeRabbitが挙げられます。異なる点はざっくり以下の通りです。

PR-AgentはCodiumAIが開発元で、有料版もあり、以下の機能が利用可能です。詳細はこちらが参考になります。値段はCodeRabbitと同じ$19/人です。

試す経緯

CodeRabbitの調査をする過程でGitHubの権限を調査したところ、GitHubのOAuth AppとGitHub Apps及びOWNER権限が必要でした。大きな組織のGitHubアカウントを運用している場合すぐの導入が難しいと感じ、OSSプロダクトも触ってみようと思ったためです。権限の詳細はこちらに記載しています。

https://dev.classmethod.jp/articles/shuntaka-investigating-coderabbit-permissions/

すでにPR-Agent周りの記事はいくつかありますが、ゼロから導入する手順はなさそうだったので書くことにしました。

より実践的な弊社の記事では以下が参考になります。

https://dev.classmethod.jp/articles/cats-2-amazon-bedrock-pr-agent-catalks/

前提

本記事でBedrockの有効化から、PR-Agentのレビューが出来るようになることを目指します。

  • Bedrockを利用
  • GitHubと連携

PR-AgentとGitHubを連携する方法は、こちらの通りざっくり2つあります。

  1. GitHub Actionsから実行
  2. GitHub Appsから実行

1.のGitHub Actionsを使う方法にします。

2.はGitHub AppsのWebHook送信先にAWS Lambdaの関数URLを使った方法が紹介されています。導入の手間が多いので本記事では見送ります。

またChrome拡張も用意されていますが、OAuth Appの連携が必要なため、本記事では検証は見送ります。
https://chromewebstore.google.com/detail/qodo-merge-ai-powered-cod/ephlnjeghhogofkifjloamocljapahnl

導入方法

Bedrockで利用したいモデルを有効化

AWSのマネジメントコンソールにアクセスし、新しい言語モデルがリリースされた際に試せるように北バージニアリージョンで有効化します。言語モデルのアクセス画面に遷移します。

CleanShot 2024-11-06 at 08.41.11@2x

モデルを有効化します。
Bedrock-1

利用したい言語モデルのチェックボックスを有効化します。
CleanShot 2024-11-05 at 12.49.23@2x

ユースケースを記載します。
CleanShot 2024-11-05 at 12.48.56@2x

問題なければ送信します。
CleanShot 2024-11-05 at 12.49.41@2x

ステータスが進行中になります。
CleanShot 2024-11-05 at 12.50.14@2x

数分待つと、アクセスが付与されたことが画面上から分かります。
CleanShot 2024-11-05 at 13.02.14@2x

有効化したモデルへアクセス出来ることを確認

AWS CLIを使って、モデルIDを取得します。

aws bedrock list-foundation-models --output table \
     --query 'modelSummaries[*].[modelId,modelName,providerName]' \
     --region us-east-1 | grep "Claude"
|  anthropic.claude-instant-v1:2:100k           |  Claude Instant                  |  Anthropic    |
|  anthropic.claude-instant-v1                  |  Claude Instant                  |  Anthropic    |
|  anthropic.claude-v2:0:18k                    |  Claude                          |  Anthropic    |
|  anthropic.claude-v2:0:100k                   |  Claude                          |  Anthropic    |
|  anthropic.claude-v2:1:18k                    |  Claude                          |  Anthropic    |
|  anthropic.claude-v2:1:200k                   |  Claude                          |  Anthropic    |
|  anthropic.claude-v2:1                        |  Claude                          |  Anthropic    |
|  anthropic.claude-v2                          |  Claude                          |  Anthropic    |
|  anthropic.claude-3-sonnet-20240229-v1:0:28k  |  Claude 3 Sonnet                 |  Anthropic    |
|  anthropic.claude-3-sonnet-20240229-v1:0:200k |  Claude 3 Sonnet                 |  Anthropic    |
|  anthropic.claude-3-sonnet-20240229-v1:0      |  Claude 3 Sonnet                 |  Anthropic    |
|  anthropic.claude-3-haiku-20240307-v1:0:48k   |  Claude 3 Haiku                  |  Anthropic    |
|  anthropic.claude-3-haiku-20240307-v1:0:200k  |  Claude 3 Haiku                  |  Anthropic    |
|  anthropic.claude-3-haiku-20240307-v1:0       |  Claude 3 Haiku                  |  Anthropic    |
|  anthropic.claude-3-opus-20240229-v1:0:12k    |  Claude 3 Opus                   |  Anthropic    |
|  anthropic.claude-3-opus-20240229-v1:0:28k    |  Claude 3 Opus                   |  Anthropic    |
|  anthropic.claude-3-opus-20240229-v1:0:200k   |  Claude 3 Opus                   |  Anthropic    |
|  anthropic.claude-3-opus-20240229-v1:0        |  Claude 3 Opus                   |  Anthropic    |
|  anthropic.claude-3-5-sonnet-20240620-v1:0    |  Claude 3.5 Sonnet               |  Anthropic    |
|  anthropic.claude-3-5-sonnet-20241022-v2:0    |  Claude 3.5 Sonnet v2            |  Anthropic    |
|  anthropic.claude-3-5-haiku-20241022-v1:0     |  Claude 3.5 Haiku                |  Anthropic    |

今回はclaudeだけですが、言語モデルの複数のプロバイダーのAPIラッパーであるlitellmを利用します。

pip3 install litellm

有効化した言語モデルと有効化していないモデルを指定して、動作確認を実施します。

  • anthropic.claude-3-sonnet-20240229-v1:0 (有効化済み)
  • anthropic.claude-3-haiku-20240307-v1:0 (有効化済み)
  • anthropic.claude-3-opus-20240229-v1:0 (有効化していない)
Bedrockで有効化したモデルの動作確認をするスクリプト
from litellm import completion
from typing import Dict, List
import time
from datetime import datetime

def test_claude_models():
    models = [
        {"id": "anthropic.claude-3-sonnet-20240229-v1:0", "name": "Claude 3 Sonnet"}, # 成功する前提
        {"id": "anthropic.claude-3-haiku-20240307-v1:0", "name": "Claude 3 Haiku"}, # 成功する前提
        {"id": "anthropic.claude-3-opus-20240229-v1:0", "name": "Claude 3 Opus"}, # 失敗する前提
    ]

    results: List[Dict] = []
    test_message = "Hello, how are you?"

    for model in models:
        print(f"\nTesting {model['name']} ({model['id']})...")
        try:
            start_time = time.time()

            response = completion(
                model=model["id"], messages=[{"content": test_message, "role": "user"}]
            )

            end_time = time.time()
            response_time = end_time - start_time

            result = {
                "model_name": model["name"],
                "model_id": model["id"],
                "status": "success",
                "response": response.choices[0].message.content
                if response.choices
                else None,
                "response_time": f"{response_time:.2f}s",
                "timestamp": datetime.now().isoformat(),
            }

        except Exception as e:
            result = {
                "model_name": model["name"],
                "model_id": model["id"],
                "status": "error",
                "error": str(e),
                "timestamp": datetime.now().isoformat(),
            }

        results.append(result)
        print(f"Status: {result['status']}")
        if result["status"] == "success":
            print(f"Response time: {result['response_time']}")
            print(f"Response: {result['response'][:100]}...")
        else:
            print(f"Error: {result['error']}")

    return results

if __name__ == "__main__":
    results = test_claude_models()

    # 結果の集計を表示
    print("\n=== Summary ===")
    success_count = sum(1 for r in results if r["status"] == "success")
    print(f"Total models tested: {len(results)}")
    print(f"Successful requests: {success_count}")
    print(f"Failed requests: {len(results) - success_count}")

    # 成功したモデルの平均応答時間を計算
    response_times = [
        float(r["response_time"].rstrip("s"))
        for r in results
        if r["status"] == "success"
    ]
    if response_times:
        avg_response_time = sum(response_times) / len(response_times)
        print(f"Average response time: {avg_response_time:.2f}s")

期待通りの結果が得られました。

$ python3 check_model.py

Testing Claude 3 Sonnet (anthropic.claude-3-sonnet-20240229-v1:0)...
Status: success
Response time: 6.42s
Response: Hello! As an AI language model, I don't have feelings or emotions, but I'm operating properly and re...

Testing Claude 3 Haiku (anthropic.claude-3-haiku-20240307-v1:0)...
Status: success
Response time: 1.24s
Response: Hello! As an AI assistant, I don't have feelings or a physical form, but I'm functioning properly an...

Testing Claude 3 Opus (anthropic.claude-3-opus-20240229-v1:0)...

Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm.set_verbose=True'.

Status: error
Error: litellm.BadRequestError: BedrockException - {"message":"Invocation of model ID anthropic.claude-3-opus-20240229-v1:0 with on-demand throughput isn’t supported. Retry your request with the ID or ARN of an inference profile that contains this model."}

=== Summary ===
Total models tested: 3
Successful requests: 2
Failed requests: 1
Average response time: 3.83s

GitHub ActionsとBedrock連携用のIAMロールの作成

bedrockの権限を付与して、PRレビュー出来る状態にします。

import { Stack, aws_iam } from 'aws-cdk-lib';
import type { Construct } from 'constructs';

const gitHubOwner = 'shuntaka9576';
const gitHubRepo = 'pr-agent-sample';

export class GitHubActionOIDCStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const accountId = Stack.of(this).account;

    new aws_iam.OpenIdConnectProvider(this, 'GitHubIdProvider', {
      url: 'https://token.actions.githubusercontent.com',
      clientIds: ['sts.amazonaws.com'],
    });

    new aws_iam.Role(this, 'GitHubActionsOidcRole', {
      roleName: 'github-action-assume-role', // <-- 後ほどGitHub Actionsの定義で指定するロール名
      assumedBy: new aws_iam.FederatedPrincipal(
        `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`,
        {
          StringEquals: {
            'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
          },
          StringLike: {
            'token.actions.githubusercontent.com:sub': `repo:${gitHubOwner}/${gitHubRepo}:*`,
          },
        },
        'sts:AssumeRoleWithWebIdentity'
      ),
      managedPolicies: [
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AWSCloudFormationFullAccess'
        ),
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('IAMFullAccess'),
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'),
      ],
      inlinePolicies: {
        ssm: new aws_iam.PolicyDocument({
          statements: [
            new aws_iam.PolicyStatement({
              effect: aws_iam.Effect.ALLOW,
              actions: [
                'ssm:GetParameters',
                'ssm:GetParameter',
                'ssm:PutParameter',
              ],
              resources: ['*'],
            }),
          ],
        }),
        bedrock: new aws_iam.PolicyDocument({
          statements: [
            new aws_iam.PolicyStatement({
              effect: aws_iam.Effect.ALLOW,
              actions: [
                'bedrock:InvokeModel',
                'bedrock:InvokeModelWithResponseStream',
              ],
              resources: ['arn:aws:bedrock:*::foundation-model/anthropic.*'],
            }),
          ],
        }),
      },
    });
  }
}

GitHub Actionsを作成

公式を参考にGitHub Actionsの定義を作成します。

.github/workflows/pr-review.yml
on:
  pull_request:
    types: [opened, reopened, ready_for_review]
  issue_comment:
jobs:
  pr_agent_job:
    if: ${{ github.event.sender.type != 'Bot' }}
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
      contents: write
      id-token: write
    name: Run pr agent on every pull request, respond to user comments
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-action-assume-role # <- CDKで作成したロール名を指定
          aws-region: us-east-1
      - name: PR Agent action step
        id: pragent
        uses: Codium-ai/pr-agent@main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

AWS_ACCOUNT_IDを登録します。

gh variable set AWS_ACCOUNT_ID -b "value"

PR-Agentのコンフィグ設定を作成

サポートされているモデル定義は、こちらを参考にします。モデルにClaude 3 Sonnet、フォールバックモデルにClaude 3 Haikuを指定します。このファイルをリポジトリルートに配備します。

.pr_agent.toml[config]
model = "bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
model_turbo = "bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
fallback_models = ["bedrock/anthropic.claude-3-haiku-20240307-v1:0"]

[pr_reviewer]
extra_instructions = "answer in Japanese"

[pr_description]
extra_instructions = "answer in Japanese"

[pr_code_suggestions]
extra_instructions = "answer in Japanese"

[pr_add_docs]
extra_instructions = "answer in Japanese"

[pr_questions]
extra_instructions = "answer in Japanese"

[pr_update_changelog]
extra_instructions = "answer in Japanese"

[pr_test]
extra_instructions = "answer in Japanese"

[pr_improve_component]
extra_instructions = "answer in Japanese"

レビューの動作確認

HonoのAPIを追加する簡単なPRを作成します。2分でレビューは完了しました。

実際のPRはこちらにありますので、直接見ていただくのが良いと思います。

PRサマリの作成
CleanShot 2024-11-06 at 12.51.59@2x

レビューでは、Best PracticeMaintainabilityの観点で指摘がありました。サンプルコードなのであまりコンテキストがなくあまり参考にはなりませんが、外した指摘とは言えず個人的には参考になりました。
CleanShot 2024-11-06 at 12.56.42@2x
CleanShot 2024-11-06 at 12.57.23@2x
CleanShot 2024-11-06 at 12.59.23@2x

さいごに

レビューコメントの付与方法に関しては、GitHubのMulti-line code suggestionsを使っているCodeRabbitの方に軍配があるなと感じました。この方式だと指摘に対する対応が1対1で管理できるのが整理しやすく便利です。

今現在すぐに少しでもレビューの負荷を下げたい場合は、導入ハードル(AWSとGitHubで完結し、従量課金)が低いので良い選択肢ではないかなと思います。シュッと入れて実際のコードで検証し、刺さればPro版やCodeRabbitを検討する形が良いと思います!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.